//--------------------------------------------------------------------------
// 
//  Copyright (c) Chili Software.  All rights reserved. 
// 
//  File: MappingBuilder.cs
//
//  Description: Creates the mapping for a persistent.
// 
//--------------------------------------------------------------------------

using System;
using System.Reflection;
using Chili.Opf3.Relations;
using System.Collections.Generic;

namespace Chili.Opf3.Mapping.Providers
{
	/// <summary>
	/// Class that creates the mapping for one persistent object type.
	/// </summary>
	/// <remarks>
	/// This class creates the mapping for one persistent object type. It allows to create the relation 
	/// and the member mappings.
	/// </remarks>
	internal static class MappingBuilder
	{
		private const string RELATION_IDENTIFIER = "72AC52C6-3D52-491e-883B-836F8508376F+";

		private static TypeMappingProviderCollection _providers = new TypeMappingProviderCollection();
		private static bool _autoInjectDynamicPopulateHelper = true;

		/// <summary>
		/// Static constructor.
		/// </summary>
		static MappingBuilder()
		{
			_providers.Add(new AttributeTypeMappingProvider());
		}

		/// <summary>
		/// Gets the classes that implements the <see cref="ITypeMappingProvider">ITypeMappingProvider</see>
		/// interfaces.
		/// </summary>
		public static TypeMappingProviderCollection TypeMappingProviders
		{
			get { return _providers; }
		}

		/// <summary>
		/// Gets or sets whether the framework should inject code in each persistent object to improve the performance
		/// of loading and saving of persistent objects. This can also be achieved by marking each persistent object that should
		/// get code injected with the <see cref="IDynamicPopulateHelper">IDynamicPopulateHelper</see> interface.
		/// </summary>
		public static bool AutoInjectDynamicPopulateHelper
		{
			get { return _autoInjectDynamicPopulateHelper; }
			set { _autoInjectDynamicPopulateHelper = value; }
		}

		#region CreatePersistentMapping

		/// <summary>
		/// Creates the mapping for the given persistent type.
		/// </summary>
		/// <param name="type">The type the mapping is created for.</param>
		public static TypeMapping CreatePersistentMapping(Type type)
		{
			if (type == null)
				throw new ArgumentNullException("type");

			// Throw an exception if the type is an interface.
			if (type.IsInterface)
			{
				throw new MappingException(
					string.Format("Cannot create mapping for interface '{0}'! Please use TypeMapping.RegisterPersistentInterface to register the interface with persistent type.", type.FullName));
			}
			// Notify the developer that the persistent mapping is going to be created.
			Tracing.Current.TraceVerbose("Creating mapping for persistent object. Persistent object type: " + type.ToString());

			// Get the persistent attribute.
			PersistentAttribute attribute = _providers.GetPersistentAttribute(type);
			if (attribute == null)
				throw new MappingException(string.Format("Cannot create mapping for type '{0}' without persistent attribute.", type.FullName));

			// Create the list of member mappings.
			MemberInfoCollection members = new MemberInfoCollection();
			CreateMemberMappings(type, members);

			return new TypeMapping(type, attribute, members);
		}

		#region CreateMemberMappings

		/// <summary>
		/// Creates the mapping between the member items and the fields in the storage.
		/// </summary>
		/// <param name="type">The type that's mappings are created.</param>
		/// <param name="list">The list of mapping items.</param>
		private static void CreateMemberMappings(Type type, MemberInfoCollection list)
		{
			if (list == null)
				throw new ArgumentNullException("list");

			// Return if we reached the object.
			if (type == null || type == typeof(object))
				return;

			// Loop over all members to analyze them.
			foreach (MemberInfo member in type.GetMembers(BindingFlags.Public | BindingFlags.NonPublic |
				BindingFlags.Instance | BindingFlags.DeclaredOnly))
			{
				// Get the field attribute of the member.
				FieldAttribute fieldAttribute = _providers.GetMemberFieldAttribute(type, member);
				if (fieldAttribute == null)
					continue;

				// Notify the developer that the object mapping for the persistent is created.
				Tracing.Current.TraceVerbose(string.Format("Creating mapping between persistent object member '{0}' and storage field '{1}'", member.Name, fieldAttribute.FieldName));

				// Get the member type.
				Type memberType = GetMemberType(type, member);

				IMemberInfo info = null;
				bool isPopulateHelperEx = false;

				// Set the populate helper extended to true, if the interface is implemented.
				if (typeof(IPopulateHelperExtended).IsAssignableFrom(type))
					isPopulateHelperEx = true;

				if (!isPopulateHelperEx && typeof(IPopulateHelper).IsAssignableFrom(type))
				{
					// Check which type of IMemberInfo to create. Currently mapping to properties and fields is supported.
					switch (member.MemberType)
					{
						case MemberTypes.Field:
						case MemberTypes.Property:
							info = new PopulateHelperMetaInfo(member, memberType, fieldAttribute);
							break;
						default:
							throw new MappingException("Member type is not supported for mapping.");
					}
				}
				else if ((MappingBuilder.AutoInjectDynamicPopulateHelper || typeof(IDynamicPopulateHelper).IsAssignableFrom(type))
					&& (member is PropertyInfo))
				{
					// Check which type of IMemberInfo to create. Currently only mapping to properties is supported.
					switch (member.MemberType)
					{
						case MemberTypes.Property:
							info = new DynamicPopulateHelperMetaInfo(member, memberType, fieldAttribute);
							break;
						default:
							throw new MappingException("Member type is not supported for mapping. Member type 'Field' is not supported by IDynamicPopulateHelper!");
					}
				}
				else
				{
					// Check which type of IMemberIfno to create. Currently mapping to properties and fields is supported.
					switch (member.MemberType)
					{
						case MemberTypes.Field:
							info = new FieldMetaInfo((FieldInfo)member, memberType, fieldAttribute);
							break;
						case MemberTypes.Property:
							info = new PropertyMetaInfo((PropertyInfo)member, memberType, fieldAttribute);
							break;
						default:
							throw new MappingException("Member type is not supported for mapping.");
					}
				}

				// Create the PopulateHelperExtendedMetaInfo for the extended populate helper.
				if (isPopulateHelperEx)
					info = new PopulateHelperExtendedMetaInfo(member, memberType, fieldAttribute, info);

				// Add the created object to the list.
				list.Add(info);
			}

			// Create the mapping for the base class.
			CreateMemberMappings(type.BaseType, list);
		}

		/// <summary>
		/// Get the type of the member.
		/// </summary>
		/// <param name="type">The type who's mapping is created.</param>
		/// <param name="member">The current member.</param>
		/// <returns></returns>
		private static Type GetMemberType(Type type, MemberInfo member)
		{
			Type memberType = null;
			// Get the type of the member.
			switch (member.MemberType)
			{
				case MemberTypes.Field:
					memberType = ((FieldInfo)member).FieldType;
					break;
				case MemberTypes.Property:
					memberType = ((PropertyInfo)member).PropertyType;
					break;
				default:
					throw new MappingException("Member type is not supported for mapping.");
			}

			// If the member is not an interface get the type of the field and return it.
			if (memberType.IsInterface)
			{
				// Get an instance of the given type.
				object instance = ObjectPool.Current.CreateObject(type, null);

				// Get the value of the member.
				object value = null;
				switch (member.MemberType)
				{
					case MemberTypes.Field:
						value = ((FieldInfo)member).GetValue(instance);
						break;
					case MemberTypes.Property:
						value = ((PropertyInfo)member).GetValue(instance, null);
						break;
					default:
						throw new MappingException("Member type is not supported for mapping.");
				}

				// Return the type of the value.
				if (value != null)
					return value.GetType();
			}

			return memberType;
		}

		#endregion

		#endregion

		#region CreateRelationMappings

		/// <summary>
		/// Creates the mapping between the relation items and the member items.
		/// </summary>
		/// <param name="type">The type that's mappings are created.</param>
		/// <param name="list">The list of relation items.</param>
		public static void CreateRelationMappings(Type type, RelationInfoCollection list)
		{
			if (list == null)
				throw new ArgumentNullException("list");
			
			if (type == typeof(object))
				return;

			int counter = 0;

			// Loop over all fields to get the field attribute.
			foreach (MemberInfo member in type.GetMembers(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance |
				BindingFlags.DeclaredOnly))
			{
				// get the type for the field.
				Type memberType = null;
				switch (member.MemberType)
				{
					case MemberTypes.Property:
						memberType = ((PropertyInfo)member).PropertyType;
						break;
					case MemberTypes.Field:
						memberType = ((FieldInfo)member).FieldType;
						break;
					default: 
						continue;
				}

				if (memberType.IsSubclassOf(typeof(ObjectHolderBase)))
				{
					// Get the generic type.
					Type genericType = memberType.GetGenericArguments()[0];

					// Get the field attribute for the member.
					RelationAttribute attribute = _providers.GetMemberRelationAttribute(type, member, genericType);
					if (attribute == null || genericType == null)
						continue;

					// give the relation a name if it has none.
					if (attribute.Name == null)
					{
						attribute.Name = RELATION_IDENTIFIER + counter;
						counter++;
					}

					// Add the relation mapping.
					RelationInfo info = ParseRelationAttribute(attribute, member, type, genericType);
					list.Add(genericType, attribute, info);
				}
			}

			// Create the mapping for the base class.
			CreateRelationMappings(type.BaseType, list);
		}


		/// <summary>
		/// Extracts the data from the <see cref="RelationAttribute">RelationAttribute</see>.
		/// </summary>
		/// <param name="attribute">The <see cref="RelationAttribute">RelationAttribute</see> that is parsed.</param>
		/// <param name="member">The member exposing the relation.</param>
		/// <param name="type">The type of the parent object.</param>
		/// <param name="genericType">The type of the related objects.</param>
		private static RelationInfo ParseRelationAttribute(RelationAttribute attribute, MemberInfo member, Type type, Type genericType)
		{
			if (attribute == null)
				throw new ArgumentNullException("attribute");
			if (member == null)
				throw new ArgumentNullException("member");
			if (type == null)
				throw new ArgumentNullException("type");
			if (genericType == null)
				throw new ArgumentNullException("genericType");

			RelationInfo info = null;
			if (attribute.WeakPersistent != null)
			{
				// Create the RelationInfo class for the parsed relation.
				IList<RelatedPropertiesPair> relations = ParseRelations(attribute.Relations, type, attribute.WeakPersistent);
				IList<RelatedPropertiesPair> weakRelations = ParseRelations(attribute.WeakRelations, attribute.WeakPersistent, genericType);

				info = new RelationInfo(member, attribute, relations, weakRelations);
				info.ParentTypeMapping = TypeMapping.GetTypeMapping(type);
				info.WeakTypeMapping = TypeMapping.GetTypeMapping(attribute.WeakPersistent);
				info.ChildTypeMapping = TypeMapping.GetTypeMapping(genericType);
			}
			else
			{
				// Create the RelationInfo class for the parsed relation.
				IList<RelatedPropertiesPair> relations = ParseRelations(attribute.Relations, type, genericType);

				info = new RelationInfo(member, attribute, relations);
				info.ParentTypeMapping = TypeMapping.GetTypeMapping(type);
				info.ChildTypeMapping = TypeMapping.GetTypeMapping(genericType);
			}

			// Return the RelationInfo object instance.
			return info;
		}

		/// <summary>
		/// Parses the relation given and creates a list of relation objects.
		/// </summary>
		/// <param name="value">A string that contains relations.</param>
		/// <param name="childType">The type of the parent object.</param>
		/// <param name="parentType">The type of the child object.</param>
		private static IList<RelatedPropertiesPair> ParseRelations(string value, Type parentType, Type childType)
		{
			List<RelatedPropertiesPair> result = new List<RelatedPropertiesPair>();

			// Get the type mappings of the parent and the child type.
			TypeMapping parentMapping = TypeMapping.GetTypeMapping(parentType);
			TypeMapping childMapping = TypeMapping.GetTypeMapping(childType);

			string[] relations = value.Split(';');
			// Loop over all relations and extract the required data.
			foreach (string relation in relations)
			{
				string[] properties = relation.Split('=');
				if (properties.Length != 2)
					throw new RelationsParserException("More than two properties found in a relation.");

				// Get the name of the parent element and the name of the child element.
				string parent = properties[0].Trim();
				string child = properties[1].Trim();

				if (parent.Contains(" "))
					throw new RelationsParserException("Parent object member can't contain space.");
				if (child.Contains(" "))
					throw new RelationsParserException("Child object member can't contain space.");

				IMemberInfo parentInfo = parentMapping.Members.GetMemberInfoByName(parent);
				IMemberInfo childInfo = childMapping.Members.GetMemberInfoByName(child);

				if (parentInfo == null)
				{
					throw new RelationsParserException("Parent object member is not found in " +
							parentMapping.PersistentType);
				}
				if (childInfo == null)
				{
					throw new RelationsParserException("Child object member is not found in " +
							parentMapping.PersistentType);
				}

				// Add the related properties pair to the list of pairs.
				result.Add(new RelatedPropertiesPair(parentInfo, childInfo));
			}

			return result;
		}

		#endregion
	}
}
